package mylittleinvestigations.CaseInformation;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import mylittleinvestigations.GameUi.GameUiLoader;
import mylittleinvestigations.PersistentThreads.RenderThread;
import mylittleinvestigations.Settings;
import mylittleinvestigations.Sprite;
import mylittleinvestigations.Vector2;
import org.dawnmew.jvirtualstream.VirtualFileSystem;
import org.lwjgl.LWJGLException;
import org.newdawn.slick.BigImage;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Rectangle;
import org.newdawn.slick.opengl.PNGImageData;

public class SpriteManager
{
    private Stack<String> spriteGroupNameStack;
    private HashMap<String, ArrayList<String>> spriteIdListByGroupNameHashMap;
    
    private HashMap<String, Sprite> spriteByIdHashMap;
    private HashMap<String, Image> textureByIdHashMap;
    private HashMap<String, String> textureIdByFilePathHashMap;
    
    private final Object textureByIdHashMapSyncObject = new Object();

    public SpriteManager()
    {
        this.spriteGroupNameStack = new Stack<String>();
        this.spriteIdListByGroupNameHashMap = new HashMap<String, ArrayList<String>>();
        
        this.spriteByIdHashMap = new HashMap<String, Sprite>();
        this.textureByIdHashMap = new HashMap<String, Image>();
        this.textureIdByFilePathHashMap = new HashMap<String, String>();
    }

    public void AddTexture(String id, String filePath) throws SlickException, IOException, CaseParseException
    {
        /*Sprite sprite = null;
        String existingTextureId = null;

        if (this.textureIdByFilePathHashMap.containsKey(filePath))
        {
            existingTextureId = this.textureIdByFilePathHashMap.get(filePath);
            sprite = this.GetSpriteFromId(existingTextureId);
            this.spriteByIdHashMap.put(id, sprite);
            return;
        }*/

        this.spriteByIdHashMap.put(id, new Sprite(filePath));
        this.textureIdByFilePathHashMap.put(filePath, id);
    }
    
    public void AddTexture(String id, Image texture)
    {
        synchronized (this.textureByIdHashMapSyncObject)
        {
            this.textureByIdHashMap.put(id, texture);
        }
    }

    public Sprite GetSpriteFromId(String id)
    {
        return this.spriteByIdHashMap.get(id);
    }

    public Image GetTextureFromId(String id)
    {
        synchronized (this.textureByIdHashMapSyncObject)
        {
            return this.textureByIdHashMap.get(id);
        }
    }

    public String GetTextureIdFromFilePath(String filePath)
    {
        return this.textureIdByFilePathHashMap.get(filePath);
    }
    
    public void StartGroup(String groupName)
    {
        this.spriteGroupNameStack.push(groupName);
    }
    
    public void SaveSpriteIdToCurrentGroup(String spriteId)
    {
        // If we have no group names on the stack,
        // then we'll do nothing.  This will make this sprite
        // automatically get added to the list of common sprites.
        if (this.spriteGroupNameStack.empty())
        {
            return;
        }

        ArrayList<String> spriteIdList = null;
        String currentGroupName = this.spriteGroupNameStack.peek();
        
        if (this.spriteIdListByGroupNameHashMap.containsKey(currentGroupName))
        {
            spriteIdList = this.spriteIdListByGroupNameHashMap.get(currentGroupName);
        }
        else
        {
            spriteIdList = new ArrayList<String>();
            this.spriteIdListByGroupNameHashMap.put(currentGroupName, spriteIdList);
        }
        
        spriteIdList.add(spriteId);
    }
    
    public void EndGroup()
    {
        this.spriteGroupNameStack.pop();
    }
    
    public HashMap<String, String> SaveTextures() throws SlickException, IOException, CaseParseException
    {
        HashMap<String, String> spriteSheetIdToFilePathHashMap = new HashMap<String, String>();
        ArrayList<String> fullIdList = new ArrayList<String>(Arrays.asList(this.spriteByIdHashMap.keySet().toArray(new String[0])));
        String[] textureGroupNames = this.spriteIdListByGroupNameHashMap.keySet().toArray(new String[0]);
        
        Files.createDirectory(Paths.get(Case.getInstance().getFilePath(), "SpriteSheets"));
        
        for (int i = 0; i < textureGroupNames.length; i++)
        {
            String groupName = textureGroupNames[i];
            ArrayList<String> textureIdList = this.spriteIdListByGroupNameHashMap.get(groupName);
            
            this.SaveTexturesInGroupName(groupName, textureIdList, spriteSheetIdToFilePathHashMap);
            
            fullIdList.removeAll(textureIdList);
        }
        
        this.SaveTexturesInGroupName("CommonSprites", fullIdList, spriteSheetIdToFilePathHashMap);
        
        return spriteSheetIdToFilePathHashMap;
    }
    
    public void SaveTexturesInGroupName(String groupName, List<String> textureIdList, HashMap<String, String> spriteSheetIdToFilePathHashMap) throws IOException, CaseParseException
    {
        ArrayList<SpriteIdPair> orderedSpriteList = new ArrayList<SpriteIdPair>();
        
        for (int i = 0; i < textureIdList.size(); i++)
        {
            Sprite sprite = this.spriteByIdHashMap.get(textureIdList.get(i));
            
            if (sprite.getIsUsed())
            {
                orderedSpriteList.add(new SpriteIdPair(textureIdList.get(i), sprite));
            }
        }
        
        if (orderedSpriteList.isEmpty())
        {
            return;
        }
        
        Collections.sort(orderedSpriteList);

        String spriteSheetTextureId = groupName;
        
        EntryRow entryRow = null;
        
        // If there's less than a 10% variation between the sizes of the sprites,
        // then we'll put them in a grid instead of laying them out linearly.
        boolean aboutSameSize = true;
        
        for (int i = 0; i < orderedSpriteList.size(); i++)
        {
            for (int j = i + 1; j < orderedSpriteList.size(); j++)
            {
                Sprite firstSprite = orderedSpriteList.get(i).Sprite;
                Sprite secondSprite = orderedSpriteList.get(j).Sprite;
                
                if (Math.abs(firstSprite.getWidth() / secondSprite.getWidth() - 1) >= 0.1 ||
                    Math.abs(firstSprite.getHeight() / secondSprite.getHeight() - 1) >= 0.1)
                {
                    aboutSameSize = false;
                    break;
                }
            }
            
            if (!aboutSameSize)
            {
                break;
            }
        }
        
        for (int i = 0; i < orderedSpriteList.size(); i++)
        {
            orderedSpriteList.get(i).Sprite.persistBufferedImage();
            
            if (orderedSpriteList.get(i).Sprite.getBufferedImage() == null)
            {
                continue;
            }
            
            if (entryRow == null)
            {
                entryRow = new EntryRow(orderedSpriteList.get(i), aboutSameSize, orderedSpriteList.size());
            }
            else
            {
                entryRow.AddSprite(orderedSpriteList.get(i));
            }
            
            orderedSpriteList.get(i).Sprite.disposeBufferedImage();
        }
        
        BufferedImage spriteSheetImage = new BufferedImage(entryRow.getWidth(), entryRow.getHeight(), BufferedImage.TYPE_INT_ARGB);
        java.awt.Graphics spriteSheetImageGraphics = spriteSheetImage.getGraphics();
        
        entryRow.DrawImages(spriteSheetTextureId, spriteSheetImageGraphics, orderedSpriteList);
        this.SaveImage(spriteSheetImage, spriteSheetTextureId, spriteSheetIdToFilePathHashMap);
    }
    
    public void SaveImage(BufferedImage image, String spriteSheetTextureId, HashMap<String, String> spriteSheetIdToFilePathHashMap) throws IOException
    {
        String relativeFilePath = Paths.get("SpriteSheets", spriteSheetTextureId + ".png").toString().replaceAll("\\\\","/");
        String filePath = Case.getInstance().getFilePath() + "/" + relativeFilePath;

        ImageIO.write(image, "png", new File(filePath));
        spriteSheetIdToFilePathHashMap.put(spriteSheetTextureId, relativeFilePath);
    }

    void SaveToXml(CaseXmlWriter writer) throws IOException, SlickException, CaseParseException
    {
        HashMap<String, String> spriteSheetIdToFilePathHashMap = this.SaveTextures();
        
        writer.StartElement("SpriteManager");
        
        writer.StartElement("SpriteByIdHashMap");
        ArrayList<String> idList = new ArrayList<String>(Arrays.asList(this.spriteByIdHashMap.keySet().toArray(new String[0])));
        Collections.sort(idList);
        String[] ids = idList.toArray(new String[0]);
        
        for (int i = 0; i < ids.length; i++)
        {
            String id = ids[i];
            Sprite sprite = this.spriteByIdHashMap.get(id);
            
            if (sprite.getIsUsed())
            {
                writer.StartElement("Entry");
                writer.WriteTextElement("Id", id);
                System.out.println("Now saving sprite with ID '" + id  + "'.");
                sprite.SaveToXml(writer);
                writer.EndElement();
            }
        }
        
        writer.EndElement();
        
        writer.WriteIntElement("SpriteCount", spriteSheetIdToFilePathHashMap.size());
        
        writer.StartElement("SpriteSheetIdToFilePathHashMap");
        String[] spriteSheetIds = spriteSheetIdToFilePathHashMap.keySet().toArray(new String[0]);
        
        for (int i = 0; i < spriteSheetIds.length; i++)
        {
            writer.StartElement("Entry");
            String spriteSheetId = spriteSheetIds[i];
            writer.WriteTextElement("SpriteSheetId", spriteSheetId);
            writer.WriteTextElement("FilePath", spriteSheetIdToFilePathHashMap.get(spriteSheetId));
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.EndElement();
    }

    public void LoadFromXml(CaseXmlReader reader) throws SlickException, IOException
    {
        try
        {
            this.spriteByIdHashMap = new HashMap<String, Sprite>();
            this.textureByIdHashMap = new HashMap<String, Image>();

            reader.StartElement("SpriteManager");

            reader.StartElement("SpriteByIdHashMap");
            reader.StartList("Entry");

            while (reader.MoveToNextListItem())
            {
                String id = reader.ReadTextElement("Id");
                Sprite sprite = new Sprite(reader);
                this.spriteByIdHashMap.put(id, sprite);
            }

            reader.EndElement();
            
            int totalSpriteCount = reader.ReadIntElement("SpriteCount");
            int currentSpriteCount = 0;
            
            Case.getInstance().setLoadStage("sprite sheets" + (Settings.UseSharedContext ? " (" + currentSpriteCount + " of " + totalSpriteCount + ")" : ""));

            reader.StartElement("SpriteSheetIdToFilePathHashMap");
            reader.StartList("Entry");
            
            if (Settings.UseSharedContext)
            {
                RenderThread.getSharedContext().makeCurrent();
            }

            while (reader.MoveToNextListItem())
            {
                String spriteSheetId = reader.ReadTextElement("SpriteSheetId");
                String filePath = reader.ReadTextElement("FilePath");

                if (Settings.UseSharedContext)
                {
                    PNGImageData imageData = new PNGImageData();
                    ByteBuffer byteBuffer = imageData.loadImage(VirtualFileSystem.getInstance().getInputStream(filePath));

                    BigImage bigImage = new BigImage(imageData, byteBuffer, Image.FILTER_LINEAR);
                    this.AddTexture(spriteSheetId, bigImage);

                    // Don't perform initial rendering on the Mac - it doesn't help anything.
                    if (!Settings.isMac)
                    {
                        for (int y = 0; y < bigImage.getVerticalImageCount(); y++)
                        {
                            for (int x = 0; x < bigImage.getHorizontalImageCount(); x++)
                            {
                                RenderThread.getInstance().LoadImage(bigImage.getTile(x, y));
                            }
                        }
                    }

                    try
                    {
                        Thread.sleep(10);
                    }
                    catch (InterruptedException ex)
                    {
                        Logger.getLogger(SpriteManager.class.getName()).log(Level.SEVERE, null, ex);
                    }

                    System.gc();
                    
                    currentSpriteCount++;
                    Case.getInstance().setLoadStage("sprite sheets (" + currentSpriteCount + " of " + totalSpriteCount + ")");
                }
                else
                {
                    RenderThread.getInstance().LoadImage(spriteSheetId, filePath);
                }
            }
            
            if (!Settings.isMac)
            {
                RenderThread.getInstance().FinalizeImagesToLoad();
            }
            
            if (Settings.UseSharedContext)
            {
                RenderThread.getSharedContext().releaseContext();
            }

            reader.EndElement();

            reader.EndElement();
        }
        catch (LWJGLException ex)
        {
            Logger.getLogger(SpriteManager.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    private class SpriteIdPair implements Comparable<SpriteIdPair>
    {
        public String Id;
        public Sprite Sprite;
        
        public SpriteIdPair(String id, Sprite sprite)
        {
            this.Id = id;
            this.Sprite = sprite;
        }
        
        public int compareTo(SpriteIdPair spriteIdPair)
        {
            if (this.Sprite.getHeight() != spriteIdPair.Sprite.getHeight())
            {
                return (int)(spriteIdPair.Sprite.getHeight() - this.Sprite.getHeight());
            }
            else
            {
                return (int)(spriteIdPair.Sprite.getWidth() - this.Sprite.getWidth());
            }
        }
    }
    
    private class EntryRow
    {
        private final ArrayList<EntryColumn> entryColumnList;
        
        private int totalWidth;
        private int maxHeight;
        
        public EntryRow(SpriteIdPair initialSpriteIdPair, boolean aboutSameSize, int numSprites) throws CaseParseException
        {
            this.entryColumnList = new ArrayList<EntryColumn>();
            
            this.totalWidth = 0;
            this.maxHeight = initialSpriteIdPair.Sprite.getBufferedImage().getHeight();
            
            // If all of the original sprites involved were about the same size, then
            // let's give them a nice grid to work with in that case.
            if (aboutSameSize)
            {
                this.maxHeight = (int)(this.maxHeight * Math.sqrt(numSprites));
            }
            
            this.AddSprite(initialSpriteIdPair);
        }
        
        public final void AddSprite(SpriteIdPair spriteIdPair) throws CaseParseException
        {
            boolean imageAdded = false;
            
            for (int i = 0; i < this.entryColumnList.size(); i++)
            {
                if (this.entryColumnList.get(i).TryAddSprite(spriteIdPair) == true)
                {
                    imageAdded = true;
                    break;
                }
            }
            
            if (!imageAdded)
            {
                this.entryColumnList.add(new EntryColumn(this.totalWidth, this.maxHeight, spriteIdPair));
                this.totalWidth += spriteIdPair.Sprite.getBufferedImage().getWidth();
            }
        }
        
        public int getWidth()
        {
            return this.totalWidth;
        }
        
        public int getHeight()
        {
            int largestColumnHeight = 0;
            
            for (EntryColumn entryColumn : this.entryColumnList)
            {
                if (entryColumn.getHeight() > largestColumnHeight)
                {
                    largestColumnHeight = entryColumn.getHeight();
                }
            }
            
            return largestColumnHeight;
        }
        
        public void DrawImages(String spriteSheetTextureId, java.awt.Graphics spriteSheetGraphics, ArrayList<SpriteIdPair> spriteIdPairList) throws CaseParseException
        {
            for (int i = 0; i < this.entryColumnList.size(); i++)
            {
                this.entryColumnList.get(i).DrawImages(spriteSheetTextureId, spriteSheetGraphics, spriteIdPairList);
            }
        }
    }
    
    private class EntryColumn
    {
        private final ArrayList<String> spriteIdList;
        
        private final int offsetX;
        
        private int totalHeight;
        private final int maxHeight;
        private final int maxWidth;
        
        public EntryColumn(int offsetX, int maxHeight, SpriteIdPair initialSpriteIdPair) throws CaseParseException
        {
            this.spriteIdList = new ArrayList<String>();
            
            this.offsetX = offsetX;
            
            this.totalHeight = 0;
            this.maxHeight = maxHeight;
            this.maxWidth = initialSpriteIdPair.Sprite.getBufferedImage().getWidth();
            
            if (TryAddSprite(initialSpriteIdPair) == false)
            {
                throw new CaseParseException("Something has gone seriously wrong: despite ordering by height, we've found an initial image that can't fit in a column.");
            }
        }
        
        public int getHeight()
        {
            return this.totalHeight;
        }
        
        public final boolean TryAddSprite(SpriteIdPair spriteIdPair) throws CaseParseException
        {
            if (this.totalHeight + spriteIdPair.Sprite.getBufferedImage().getHeight() > this.maxHeight ||
                spriteIdPair.Sprite.getBufferedImage().getWidth() > this.maxWidth)
            {
                return false;
            }
            
            this.totalHeight += spriteIdPair.Sprite.getBufferedImage().getHeight();
            this.spriteIdList.add(spriteIdPair.Id);
            
            return true;
        }
        
        public void DrawImages(String spriteSheetTextureId, java.awt.Graphics spriteSheetGraphics, ArrayList<SpriteIdPair> spriteIdPairList) throws CaseParseException
        {
            int curHeight = 0;
            
            for (String spriteId : this.spriteIdList)
            {
                Sprite sprite = null;
                
                for (int i = 0; i < spriteIdPairList.size(); i++)
                {
                    SpriteIdPair spriteIdPair = spriteIdPairList.get(i);
                    
                    if (spriteId.equals(spriteIdPair.Id))
                    {
                        sprite = spriteIdPair.Sprite;
                        break;
                    }
                }
                
                if (sprite != null)
                {
                    sprite.persistBufferedImage();
                    
                    BufferedImage image = sprite.getBufferedImage();
                    spriteSheetGraphics.drawImage(sprite.getBufferedImage(), this.offsetX, curHeight, null);
                    sprite.LoadSpriteSheet(spriteSheetTextureId, new Rectangle(this.offsetX, curHeight, image.getWidth(), image.getHeight()));
                    curHeight += image.getHeight();
                    
                    sprite.disposeBufferedImage();
                }
            }
        }
    }
}